跳到主要内容

谈谈 REPL

· 阅读需 13 分钟
Random Image
图片与正文无关

前言

说实话,这几篇文章就是在跟大家说,看,我又造了个轮子,实际上价值和实用性也就那么回事,对于现在这个轮子满天飞的时代,多一个轮子也没什么,所以今天暂时插播一篇技术文章,聊聊我对 REPL 的理解,至于部分对 Semo 感兴趣的同学,可以看看我挖的几个坑:


说到 REPL,基本上每个知名的编程语言都是有的,除了 Node,比如 PHP, Python 都是天然自带的,JavaGo 也有相应的解决方案可以进入到各自的 REPL 环境。

REPL 是什么?

维基百科 -- “读取-求值-输出”循环(英语:Read-Eval-Print Loop,简称 REPL),也被称做交互式顶层构件(英语:interactive toplevel),是一个简单的,交互式的编程环境。这个词常常用于指代一个 Lisp 的交互式开发环境,也能指代命令行的模式。

当你进入到 REPL,你很容易理解这是个什么东西,你可以在里面实验各种基本的语言特性,常见的比如输出个字符串,或者做个基本的运算,没有上下文,回车即看到结果,有时你可以用来做临时的一些想法的验证,比如某些算法题。

REPL 还能干什么

这里我想说的是,不同语言,不同背景的人对于 REPL 的观感是不同的,这里也能让人联想到,如果可能的话,还是应该多学习一些不同的语言,增长见识。

在学习 PHP 的时候,我们遇到了目前设计最优雅的框架 Laravel,Laravel 有个命令叫 tinker,php artisan tinker 这个 tinker 是基于 PHP 的 一个包 psysh 实现的。这些都不重要,关键是 Laravel 教会了我一件事情,就是 REPL 不仅仅是一个语言的玩具,还是开发过程中的重要助手。

而要做到这一点,需要对 REPL 进行定制,需要把业务逻辑注入到 REPL 当中,举几个例子:

  • 按任意条件查询数据库,并且直接使用 ORM 模型方法 或者纯 SQL,而不需要考虑连接问题
  • 根据模型或者工厂方法快速生成测试数据
  • 测试某个刚刚写的业务逻辑方法,给个输入看看输出
  • 看看某个 内置函数的文档
  • 调用工具类给指定人发个邮件或者消息推送
  • ...

以上提到的这些场景,可以说,没有 REPL,我们也可以使用比如接口触发的方式或者脚本触发的方式来实现,但是在我看来,都没有 REPL 来得自然。

说回 Node 的 REPL

说到 Node,就会想到是一直伴随着前端的发展而发展的,也就是说不管 Node 有多么全能,大多数时候还是会被认为是前端的工具,这一点从掘金的分类上也可见一斑。前端同学们用 Node 做了很多这个时代主流的前端方案,附带了各种命令行工具,而在 Npm 里也有大量为实现命令行工具而提供的包。当然 Node 在前端同学手里还可以完成一些 BFF 的工作,但这不是本文的重点。

其实 Node 也是可以做后端开发的,也有很多后端框架可以选择,比如 koa, express,比如 eggnest。不同的后端框架带来不同的开发体验,但目前为止,从开发流程上,我还没有看到如 PHP 的 Laravel 那样的,可以用 REPL 来辅助开发的最佳实践。

Node REPL 的扩展

通过分析官方文档,可以看到有专门的一章是关于 REPL 的,不知道大家关注的多不多,又是拿来干什么用的。我是带着上面的思考去研究这部分的,我希望 Node 的 REPL 可以用来辅助开发。

自定义 REPL 入口

如果想用 Node 自带的 REPL,定制起来比较困难,所以还是应该用自定义的入口,根据 API 来定制。比如:

// index.js
const repl = require("repl");

r = repl.start({
prompt: ">>> ",
ignoreUndefined: true,
});
r.context.a = 1;
# node index.js
>>> a
1

自定义 REPL 命令

用过 Node 的 REPL 命令的同学都知道,里面内置了几个 . 开头的命令:

.break    Sometimes you get stuck, this gets you out
.clear Alias for .break
.editor Enter editor mode
.exit Exit the repl
.help Print this help message
.load Load JS from a file into the REPL session
.save Save all evaluated commands in this REPL session to a file

这些命令一般都不太常用,但是我们怎么自定义呢,也是可以的,在上面代码的基础上,我们可以加一段进去:

r.defineCommand("hello", {
help: "Hello Command",
action(name) {
this.clearBufferedCommand();
console.log("hello", name);
this.displayPrompt();
},
});

怎么支持 await 调用异步方法

Node 后端项目里满满的异步方法,不管是访问其他服务还是访问各种数据库,缓存,消息队列等等,所以在注入不是问题的前提下(参考上面自定义入口的 context),我们需要 REPL 能够触发异步,也就是 await。

这个问题截止目前,官方最新版本 v12,仍然没有做的很好,所以需要用 hack 的方式,找了好久,找到国外网友的相关项目,改了之后发现好使,这里贴个主要思路:

 r.eval = function coEval(cmd, context, filename, callback) {
if (cmd.match(/^await\s+/) || cmd.match(/.*?await\s+/) && cmd.match(/^\s*\{/)) {
cmd = '(async function() { (' + cmd + ') })()'
}
}

可以看到真的是很 hack 的方式,但是是工作的,这里代码是不完整的,感兴趣的同学可以去翻翻 Semo源码

一些奇妙的化学变化

一旦环境中有了支持调用异步方法的能力,REPL 的想象空间就大了很多,比如把 axios 注入进来,REPL 就秒变 HTTP 客户端了等等。

另外,我还发现,给 repl 定义命令的 defineCommand 方法,传参的 action 是支持异步的(在 v12 环境里可以),也就是说,我们的自定义命令也可以异步起来。

基于插件系统扩展 REPL

难道我每次想做点什么都需要把入口文件改来改去么,一个基本的思路就是把调度逻辑和被调度的逻辑分开各自维护的思想,这里我并没有采用面向对象的设计模式,而是面向过程的,这也是整个 Semo 提供的插件机制。

每一个插件可以通过钩子的方式,向 REPL 中,注入数据和方法。

module.exports = (Utils) => {
return {
hook_repl: new Utils.Hook('semo', {
a: 1,
async b() => { console.log( 'b') }
})
}
}

不了解 Semo 的同学大概看个意思就行,就是通过这种方式,实现了一个叫 hook_repl 的钩子,为 REPL 注入了一个变量 a,和一个异步方法 b。

注入以后去哪里找呢,在 Semo 的 REPL 里,为了不过度污染顶层作用域,所以注入的信息都到了 Semo.hooks 下。

这样使用起来又比较麻烦,所以这里还提供了一个方法,Semo.extract,可以把深度的变量释放出来,比如 >>> Semo.extract(Semo.hooks),这样里面的信息就以键为变量名被释放到顶层作用域了,如果不小心,有的键跟全局对象重名,会把全局对象覆盖。

其他辅助方法

作为一个开发工具,如果代码修改了怎么办呢,这里提供了一个方法Semo.reload和一个命令.reload,作用是一样的,就是重新执行钩子,获取最新的返回值。

npm 有海量的包,日常开发过程中,我们经常有实验包功能的需求,如果每次都起一个项目还是比较麻烦的,这里提供了一个方便的方法:

>>> let _ = Semo.import('lodash')
>>> _.VERSION
4.17.x

这里以 lodash 为例,其他包同理,这里包会下载到非当前目录下的指定位置,并且下过一次就复用,除非第二个参数设置为 true, 强制下载最新版本。

作为一个定制方案,好处就是需要什么加什么,比如还有一个 Semo.run 的方法,用来提取异步方法的深度结果,比如:

Semo.run(User.findOne({ id: 1 }), "info.firstName");
Semo.run(redis.load("name"), "set", "a", "b");

实际的效果就是我可以把通常需要写多行的逻辑写在一行,在 REPL 环境下比较适用。

真实项目中的例子

在真实项目中,为了更好的在 REPL 中服务业务开发,我会把很多东西注入到 REPL 中,比如数据库,Redis 连接实例,ORM 模型,各个 RepositoryService,工具函数等,并且尽可能地放到顶层,减少记忆负担。这里举一个例子:

这是怎么实现的呢,是通过 Semo 的配置文件进行的配置,使用上面的 extract 机制。

$plugin:
semo:

# 给 REPL 顶层注入一些对象
extract:
- Semo.hooks.application
- Semo.hooks.application.models
- Semo.hooks.application.repositories
- Semo.hooks.application.services
- Semo.hooks.redis
- Semo.hooks.sequelize

小结

好啦,今天就想跟大家分享这些,相比前面几篇水文,今天的相对来说还是有点干货的,虽然按照我的本意,还是希望大家可以试试 Semo 多提宝贵意见,但是不用也没有关系,基于这样的思路,相信大家也可以定制出自己趁手的 REPL 工具,从而提高团队和项目的开发和运维效率。